#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import logging
import math
import time
from collections import defaultdict
from collections import namedtuple
from enum import Enum
from queue import PriorityQueue
from random import randint

import pygame
from pygame.locals import *

logging.getLogger().setLevel(logging.INFO)


class Side(Enum):
    BLACK = 'black'
    WHITE = 'white'


Vector = namedtuple('Vector', ['x', 'y', 'left', 'top'])


class BaseItem(pygame.sprite.Sprite):
    cont = 100

    def __init__(self, screen, image):
        pygame.sprite.Sprite.__init__(self)
        self.item_id = BaseItem._cout_incr()
        self.screen = screen
        self.image = image
        self.rect = self.image.get_rect()

    def display(self):
        self.screen.blit(self.image, self.rect)

    @staticmethod
    def _cout_incr():
        BaseItem.cont += 1
        return BaseItem.cont


class Circle(object):
    def __init__(self, screen, pos=(0, 0), color=(255, 255, 0), radius=20, width=2):
        self.screen = screen
        self.pos = pos
        self.color = color
        self.radius = radius
        self.width = width

    def display(self):
        pygame.draw.circle(self.screen, self.color, self.pos, self.radius, self.width)


def draw_circle(screen, pos, c_color=(255, 255, 0), radius=20, width=2):
    pygame.draw.circle(screen, c_color, pos, radius, width)


Dir = namedtuple("Dir", ['x', 'y'])


class Direction(Enum):
    LEFT = Dir(-1, 0)
    LEFT_UP = Dir(-1, -1)
    UP = Dir(0, -1)
    RIGHT_UP = Dir(1, -1)
    RIGHT = Dir(1, 0)
    RIGHT_DOWN = Dir(1, 1)
    DOWN = Dir(0, 1)
    LEFT_DOWN = Dir(-1, 1)


class ChessBoard(BaseItem):
    width = 480
    height = 540
    horizontal_nums = 15  # 横向格子数
    vertical_nums = 17  # 纵向格子数
    cell_size = 30
    bg_color = (180, 180, 180)
    line_color = (255, 0, 0)
    line_width = 2
    margin = 30

    def __init__(self, screen):
        bg_image = pygame.Surface([ChessBoard.width, ChessBoard.height])
        bg_image.fill(ChessBoard.bg_color)
        super().__init__(screen, image=bg_image)
        self.rect.left = 0
        self.rect.top = 0
        self.start_left = ChessBoard.margin
        self.start_top = ChessBoard.margin
        self.end_left = self.start_left + ChessBoard.cell_size * (ChessBoard.horizontal_nums - 1)
        self.end_top = self.start_top + ChessBoard.cell_size * (ChessBoard.vertical_nums - 1)
        self.boards = defaultdict(lambda: {})
        self.init_board()

    def init_board(self):
        for x in range(ChessBoard.horizontal_nums):
            for y in range(ChessBoard.vertical_nums):
                self.boards[x][y] = (self.start_top + x * ChessBoard.cell_size,
                                     self.start_left + y * ChessBoard.cell_size)

    def dist_by_pos(self, x, y):
        """
        通过左边得到上下边距
        :param x:
        :param y:
        :return:
        """
        return self.start_left + x * ChessBoard.cell_size, self.start_top + y * ChessBoard.cell_size

    def pos(self, left, top):
        """
        根据上下边距得到在棋盘的坐标
        :param left: 左边距
        :param top: 上边距
        :return: 坐标(x, y)
        """
        filter_positions = self.__filter_pos(left=left, top=top)
        if not filter_positions:
            return None
        else:
            return ChessBoard.__get_shortest(left=left, top=top, vect_list=filter_positions)

    def __filter_pos(self, left, top):
        if (left < self.start_left - ChessMain.dist_off_tolerance
                or left > self.end_left + ChessMain.dist_off_tolerance):
            return []
        if (top < self.start_top - ChessMain.dist_off_tolerance
                or top > self.end_top + ChessMain.dist_off_tolerance):
            return []
        x = (left - self.start_left) // ChessBoard.cell_size
        y = (top - self.start_top) // ChessBoard.cell_size
        if x < 0:
            x = 0
        if y < 0:
            y = 0
        filter_list = []
        for i in range(2):
            for j in range(2):
                if not ChessBoard.contains_loc(x + i, y + j):
                    continue
                else:
                    filter_list.append(Vector(x + i, y + j, self.boards[x + i][y + j][0], self.boards[x + i][y + j][1]))
        return filter_list

    @staticmethod
    def contains_loc(x, y):
        if x < 0 or y < 0:
            return False
        if x >= ChessBoard.horizontal_nums or y >= ChessBoard.vertical_nums:
            return False
        return True

    @staticmethod
    def __get_shortest(left, top, vect_list):
        for vect in vect_list:
            dist = math.hypot(vect.left - left, vect.top - top)
            if dist < ChessMain.dist_off_tolerance:
                return vect
        return None

    def display(self):
        super().display()
        self.__draw_horizonal_lines()
        self.__draw_vertical_lines()
        pygame.display.flip()

    def __draw_horizonal_lines(self):
        for i in range(ChessBoard.vertical_nums):
            pygame.draw.line(self.screen, ChessBoard.line_color,
                             (self.start_left, self.start_top + i * ChessBoard.cell_size),
                             (self.end_left, self.start_top + i * ChessBoard.cell_size), ChessBoard.line_width)

    def __draw_vertical_lines(self):
        for i in range(ChessBoard.horizontal_nums):
            pygame.draw.line(self.screen, ChessBoard.line_color,
                             (self.start_left + i * ChessBoard.cell_size, self.start_top),
                             (self.start_left + i * ChessBoard.cell_size, self.end_top), ChessBoard.line_width)


class ChessPiece(BaseItem):
    width = 20
    height = 20
    chess_img_file = {
        Side.WHITE: 'img/chess_white.png',
        Side.BLACK: 'img/chess_black.png',
    }

    def __init__(self, screen, x, y, left, top, side=Side.BLACK):
        cp_image = pygame.image.load(ChessPiece.chess_img_file[side]).convert_alpha()
        super().__init__(screen=screen, image=cp_image)
        self.side = side
        self.x = x
        self.y = y
        self.rect.center = (left, top)
        self.fingerprint = gen_fingerprint(x=self.x, y=self.y)

    def __eq__(self, other):
        return (isinstance(other, ChessPiece) and
                self.fingerprint == other.fingerprint)

    def __str__(self):
        return 'id={}, side={}, x={}, y={}, fingerprint={}, rect={}'.format(
            self.item_id, self.side, self.x, self.y, self.fingerprint, self.rect)

    __repr__ = __str__


def gen_fingerprint(x, y):
    """
    生成指纹
    :param x: 横坐标
    :param y: 纵坐标
    :return: 指纹
    """
    return x * 10000 + y


def unify_list(lst):
    """
    列表排除重复元素
    :param lst: 列表
    :return: 排除重复元素的列表
    """
    return list(set(lst))


class Robot(object):
    def __init__(self, item_manager, side=Side.BLACK):
        self.item_manager = item_manager
        self.side = side

    def choose_next(self):
        q = PriorityQueue()
        for _loc in self.item_manager.adjacent_list:
            s_weight = self.item_manager.weight_loc(x=_loc[0], y=_loc[1], side=self.side)
            o_weight = self.item_manager.weight_loc(x=_loc[0], y=_loc[1], side=self.opponent_side())
            loc_val = Robot.value_it(self_weight=s_weight, opponent_weight=o_weight)
            logging.info('%s=%d, %s=%d in pos(%d, %d), value=%d', self.side.value, s_weight,
                         self.opponent_side().value, o_weight, _loc[0], _loc[1], loc_val)
            q.put((-loc_val, randint(0, 10000), _loc[0], _loc[1]))
        if not q.empty():
            item = q.get()
            logging.info('priority loc: %s', item)
            return item[-2:]
        else:
            return ChessBoard.horizontal_nums // 2, ChessBoard.vertical_nums // 2

    def opponent_side(self):
        if self.side == Side.BLACK:
            return Side.WHITE
        else:
            return Side.BLACK

    @staticmethod
    def value_it(self_weight, opponent_weight):
        return 2 ** (2 * self_weight + 1) + 2 ** (2 * opponent_weight)


class ItemManager(object):

    def __init__(self):
        self.chess_board = None
        self.__chess_pieces = {}
        self.__winner = None
        self.current_circle = None
        self.adjacent_list = []

    def refresh(self):
        self.chess_board.display()
        if self.current_circle:
            self.current_circle.display()
        for fingerprint, chess_piece in self.__chess_pieces.items():
            chess_piece.display()
        pygame.display.flip()

    def add_chess_piece(self, chess_piece):
        if self.__winner:
            return
        if self.contains_chess_piece(chess_piece):
            return
        if not self.current_circle:
            self.current_circle = Circle(screen=ChessMain.screen, pos=chess_piece.rect.center,
                                         color=(255, 255, 0), radius=15, width=3)
        else:
            self.current_circle.pos = chess_piece.rect.center

        side_weight = self.weight_loc(x=chess_piece.x, y=chess_piece.y, side=chess_piece.side, ignore_edge=True)
        self.__judge_win(weight=side_weight, side=chess_piece.side)
        self.__chess_pieces[chess_piece.fingerprint] = chess_piece
        self.evaluate_adjacent(chess_piece)

    def __judge_win(self, weight, side):
        logging.info('%s max weight=%d', side.value, weight)
        if weight >= 4:
            self.__winner = side

    def evaluate_adjacent(self, chess_piece):
        for _dir in Direction:
            x, y = chess_piece.x + _dir.value.x, chess_piece.y + _dir.value.y
            if (ChessBoard.contains_loc(x, y)
                    and (not self.get_chess_piece_by_loc(x, y))):
                self.add_to_adjacent((x, y))
        self.rm_adjacent_by_value((chess_piece.x, chess_piece.y))
        logging.info('adjacent size: %d', len(self.adjacent_list))

    def add_to_adjacent(self, el):
        self.adjacent_list.append(el)
        self.adjacent_list = unify_list(self.adjacent_list)

    def rm_adjacent_by_value(self, el):
        if el in self.adjacent_list:
            logging.info('rm %s', el)
            self.adjacent_list.remove(el)

    def weight_loc(self, x, y, side, ignore_edge=False):
        if self.contains_in_loc(x=x, y=y):
            return 0
        my_side_chess_pieces = {k: _piece for (k, _piece) in self.__chess_pieces.items()
                                if _piece.side == side}
        opponent_side_chess_pieces = {k: _piece for (k, _piece) in self.__chess_pieces.items()
                                      if _piece.side != side}
        dir_weights = defaultdict(lambda: 0)
        for _dir in Direction:
            for i in range(1, 10):
                dx, dy = x + i * _dir.value.x, y + i * _dir.value.y
                if not ChessBoard.contains_loc(x=dx, y=dy):
                    if not ignore_edge:
                        if 0 <= dir_weights[_dir] < 4:
                            dir_weights[_dir] -= 1
                    break
                if gen_fingerprint(dx, dy) in my_side_chess_pieces:
                    dir_weights[_dir] += 1
                else:
                    if not ignore_edge:
                        if ((gen_fingerprint(dx, dy) in opponent_side_chess_pieces)
                                and (0 <= dir_weights[_dir] < 4)):
                            dir_weights[_dir] -= 1
                    break
        return ItemManager.__max_weight(dir_weights)

    @staticmethod
    def __max_weight(dir_weights):
        w_1 = dir_weights[Direction.LEFT] + dir_weights[Direction.RIGHT]
        w_2 = dir_weights[Direction.UP] + dir_weights[Direction.DOWN]
        w_3 = dir_weights[Direction.LEFT_UP] + dir_weights[Direction.RIGHT_DOWN]
        w_4 = dir_weights[Direction.RIGHT_UP] + dir_weights[Direction.LEFT_DOWN]
        return max(w_1, w_2, w_3, w_4)

    def contains_in_loc(self, x, y):
        return gen_fingerprint(x=x, y=y) in self.__chess_pieces.keys()

    def contains_chess_piece(self, chess_piece):
        return self.contains_in_loc(x=chess_piece.x, y=chess_piece.y)

    def __get_chess_piece(self, fingerprint):
        return self.__chess_pieces.get(fingerprint, None)

    def get_chess_piece_by_loc(self, x, y):
        return self.__get_chess_piece(fingerprint=gen_fingerprint(x, y))

    def is_game_over(self):
        if not self.__winner:
            return False, None
        else:
            return True, self.__winner


class TextPrinter(object):
    def __init__(self, screen, font_size=20):
        self.screen = screen
        self.text_font = pygame.font.Font(None, font_size)

    def display(self, text, left=0, top=0, font_color=(255, 255, 0)):
        text_image = self.text_font.render(text, True, font_color)
        text_rect = text_image.get_rect()
        text_rect.left = left
        text_rect.top = top
        ChessMain.screen.blit(text_image, text_rect)


class ChessMain(object):
    border_width = 480
    border_height = 540
    dist_off_tolerance = 12  # 距离偏离容忍度
    screen = None

    def __init__(self):
        pygame.init()
        pygame.font.init()
        pygame.display.set_caption("Five Chess")
        ChessMain.screen = pygame.display.set_mode((ChessMain.border_width, ChessMain.border_height), 0, 32)
        self.text_printer = TextPrinter(screen=ChessMain.screen, font_size=30)
        self.item_manager = ItemManager()
        self.robot = Robot(item_manager=self.item_manager, side=Side.BLACK)
        logging.info('init five chess game finished!')

    def _hint_if_terminal(self):
        is_over, winner = self.item_manager.is_game_over()
        if is_over:
            self.text_display(text='winner is {}'.format(winner.value))

    def robot_do(self):
        x, y = self.robot.choose_next()
        left, top = self.item_manager.chess_board.dist_by_pos(x=x, y=y)
        chess_piece = ChessPiece(screen=ChessMain.screen, x=x, y=y,
                                 left=left, top=top, side=self.robot.side)
        self.item_manager.add_chess_piece(chess_piece)

    def human_do(self, x, y):
        left, top = self.item_manager.chess_board.dist_by_pos(x=x, y=y)
        chess_piece = ChessPiece(screen=ChessMain.screen, x=x, y=y,
                                 left=left, top=top, side=Side.WHITE)
        self.item_manager.add_chess_piece(chess_piece)

    def text_display(self, text, left=0, top=0):
        self.text_printer.display(text=text, left=left, top=top, font_color=(255, 255, 0))

    @staticmethod
    def quit():
        pygame.quit()
        logging.info('game terminated!')
        exit()

    def start(self):
        logging.info('start game')
        self.item_manager.chess_board = ChessBoard(screen=ChessMain.screen)
        while True:
            try:
                self.item_manager.refresh()
                for event in pygame.event.get():
                    if event.type == QUIT:
                        ChessMain.quit()
                    if event.type == KEYDOWN:
                        if event.key == K_ESCAPE:
                            ChessMain.quit()
                    if event.type == pygame.MOUSEBUTTONUP:
                        pos = pygame.mouse.get_pos()
                        norm_pos = self.item_manager.chess_board.pos(pos[0], pos[1])
                        logging.info('pos: %s', norm_pos)
                        if norm_pos:
                            if self.item_manager.contains_in_loc(norm_pos.x, norm_pos.y):
                                logging.info('position(%s, %s) already have been occupied by [%s]',
                                             norm_pos.x, norm_pos.y,
                                             self.item_manager.get_chess_piece_by_loc(x=norm_pos.x, y=norm_pos.y))
                            else:
                                self.human_do(x=norm_pos.x, y=norm_pos.y)
                                self.robot_do()
                self._hint_if_terminal()
                pygame.display.update()
                time.sleep(0.1)
            except Exception as e:
                logging.error('Error:', e)
                self.quit()


if __name__ == '__main__':
    ChessMain().start()
